﻿//********************************** Banshee Engine (www.banshee3d.com) **************************************************//
//**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************//
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using bs;

namespace bs.Editor
{
    /** @addtogroup Script
     *  @{
     */

    /// <summary>
    /// Type of assemblies that may be generated by the script compiler.
    /// </summary>
    public enum ScriptAssemblyType
    {
        Game, Editor
    }

    /// <summary>
    /// Data required for compiling a single assembly.
    /// </summary>
    public struct CompileData
    {
        public string[] files;
        public string defines;
    }

    /// <summary>
    /// Compiles script files in the project into script assemblies.
    /// </summary>
    public static class ScriptCompiler
    {
        /// <summary>
        /// Starts compilation of the script files in the project for the specified assembly for the specified platform.
        /// </summary>
        /// <param name="type">Type of the assembly to compile. This determines which script files are used as input.</param>
        /// <param name="platform">Platform to compile the assemblies for.</param>
        /// <param name="debug">Determines should the assemblies contain debug information.</param>
        /// <param name="outputDir">Absolute path to the directory where to output the assemblies.</param>
        /// <returns>Compiler instance that contains the compiler process. Caller must ensure to properly dispose
        ///          of this object when done.</returns>
        public static CompilerInstance CompileAsync(ScriptAssemblyType type, PlatformType platform, bool debug, string outputDir)
        {
            LibraryEntry[] scriptEntries = ProjectLibrary.Search("*", new ResourceType[] { ResourceType.ScriptCode });

            List<string> scriptFiles = new List<string>();
            for (int i = 0; i < scriptEntries.Length; i++)
            {
                if(scriptEntries[i].Type != LibraryEntryType.File)
                    continue;

                FileEntry fileEntry = (FileEntry)scriptEntries[i];

                ScriptCodeImportOptions io = (ScriptCodeImportOptions) fileEntry.Options;
                if (io.EditorScript && type == ScriptAssemblyType.Editor ||
                    !io.EditorScript && type == ScriptAssemblyType.Game)
                {
                    scriptFiles.Add(Path.Combine(ProjectLibrary.ResourceFolder, scriptEntries[i].Path));
                }
            }

            string[] assemblyFolders;
            string[] assemblies;
            string outputFile;

            string builtinAssemblyPath = debug
                    ? EditorApplication.BuiltinDebugAssemblyPath
                    : EditorApplication.BuiltinReleaseAssemblyPath;

            string[] frameworkAssemblies = BuildManager.GetFrameworkAssemblies(platform);
            if (type == ScriptAssemblyType.Game)
            {
                assemblyFolders = new string[]
                {
                    builtinAssemblyPath, 
                    EditorApplication.FrameworkAssemblyPath
                };

                assemblies = new string[frameworkAssemblies.Length + 1];
                assemblies[assemblies.Length - 1] = EditorApplication.EngineAssemblyName;

                outputFile = Path.Combine(outputDir, EditorApplication.ScriptGameAssemblyName);
            }
            else
            {
                assemblyFolders = new string[]
                {
                    builtinAssemblyPath, 
                    EditorApplication.FrameworkAssemblyPath,
                    EditorApplication.ScriptAssemblyPath
                };

                assemblies = new string[frameworkAssemblies.Length + 3];
                assemblies[assemblies.Length - 1] = EditorApplication.EngineAssemblyName;
                assemblies[assemblies.Length - 2] = EditorApplication.EditorAssemblyName;
                assemblies[assemblies.Length - 3] = EditorApplication.ScriptGameAssemblyName;

                outputFile = Path.Combine(outputDir, EditorApplication.ScriptEditorAssemblyName);
            }

            Array.Copy(frameworkAssemblies, assemblies, frameworkAssemblies.Length);

            string defines = BuildManager.GetDefines(platform);
            return new CompilerInstance(scriptFiles.ToArray(), defines, assemblyFolders, assemblies, debug, outputFile);
        }
    }

    /// <summary>
    /// Represents a started compiler process used for compiling a set of script files into an assembly.
    /// </summary>
    public class CompilerInstance
    {
        private Process process;
        private Thread readErrorsThread;

        private List<CompilerMessage> errors = new List<CompilerMessage>();
        private List<CompilerMessage> warnings = new List<CompilerMessage>();

        private Regex compileErrorRegex = new Regex(@"\s*(?<file>.*)\(\s*(?<line>\d+)\s*,\s*(?<column>\d+)\s*\)\s*:\s*(?<type>warning|error)\s+(.*):\s*(?<message>.*)");
        private Regex compilerErrorRegex = new Regex(@"\s*error[^:]*:\s*(?<message>.*)");

        /// <summary>
        /// Creates a new compiler process and starts compilation of the provided files.
        /// </summary>
        /// <param name="files">Absolute paths to all the C# script files to compile.</param>
        /// <param name="defines">A set of semi-colon separated defines to provide to the compiler.</param>
        /// <param name="assemblyFolders">A set of folders containing the assemblies referenced by the script files.</param>
        /// <param name="assemblies">Names of the assemblies containing code referenced by the script files.</param>
        /// <param name="debugBuild">Determines should the assembly be compiled with additional debug information.</param>
        /// <param name="outputFile">Absolute path to the assembly file to generate.</param>
        internal CompilerInstance(string[] files, string defines, string[] assemblyFolders, string[] assemblies,
            bool debugBuild, string outputFile)
        {
            ProcessStartInfo procStartInfo = new ProcessStartInfo();
            StringBuilder argumentsBuilder = new StringBuilder();

            argumentsBuilder.Append("\"" + EditorApplication.CompilerPath + "\"");

            string monoDir = Path.GetDirectoryName(EditorApplication.CompilerPath);
            monoDir = Path.Combine(monoDir, "../");

            argumentsBuilder.Append(" \"" + monoDir + "\"");
            argumentsBuilder.Append(" -noconfig");

            if (!string.IsNullOrEmpty(defines))
                argumentsBuilder.Append(" -d:" + defines);

            if (assemblyFolders != null && assemblyFolders.Length > 0)
            {
                argumentsBuilder.Append(" -lib:\"");

                for (int i = 0; i < assemblyFolders.Length - 1; i++)
                    argumentsBuilder.Append(assemblyFolders[i] + ",");

                argumentsBuilder.Append(assemblyFolders[assemblyFolders.Length - 1] + "\"");
            }

            if (assemblies != null && assemblies.Length > 0)
            {
                argumentsBuilder.Append(" -r:");

                for (int i = 0; i < assemblies.Length - 1; i++)
                    argumentsBuilder.Append(assemblies[i] + ",");

                argumentsBuilder.Append(assemblies[assemblies.Length - 1]);
            }

            if (debugBuild)
                argumentsBuilder.Append(" -debug+ -o-");
            else
                argumentsBuilder.Append(" -debug- -o+");

            argumentsBuilder.Append(" -target:library -out:" + "\"" + outputFile + "\"");

            for (int i = 0; i < files.Length; i++)
                argumentsBuilder.Append(" \"" + files[i] + "\"");

            if (File.Exists(outputFile))
                File.Delete(outputFile);

            string outputDir = Path.GetDirectoryName(outputFile);
            if (!Directory.Exists(outputDir))
                Directory.CreateDirectory(outputDir);

            procStartInfo.Arguments = argumentsBuilder.ToString();
            procStartInfo.CreateNoWindow = true;
            procStartInfo.FileName = EditorApplication.MonoExecPath;
            procStartInfo.RedirectStandardError = true;
            procStartInfo.RedirectStandardOutput = false;
            procStartInfo.UseShellExecute = false;
            procStartInfo.WorkingDirectory = EditorApplication.ProjectPath;

            process = new Process();
            process.StartInfo = procStartInfo;
            process.Start();

            readErrorsThread = new Thread(ReadErrorStream);
            readErrorsThread.Start();
        }

        /// <summary>
        /// Worker thread method that continually checks for compiler error messages and warnings.
        /// </summary>
        private void ReadErrorStream()
        {
            while (true)
            {
                if (process == null || process.HasExited)
                    return;

                string line = process.StandardError.ReadLine();
                if (string.IsNullOrEmpty(line))
                    continue;

                CompilerMessage message;
                if (TryParseCompilerMessage(line, out message))
                {
                    if (message.type == CompilerMessageType.Warning)
                    {
                        lock (warnings)
                            warnings.Add(message);
                    }
                    else if (message.type == CompilerMessageType.Error)
                    {
                        lock (errors)
                            errors.Add(message);
                    }
                }
            }
        }

        /// <summary>
        /// Parses a compiler error or warning message into a more structured format.
        /// </summary>
        /// <param name="messageText">Text of the error or warning message.</param>
        /// <param name="message">Parsed structured version of the message.</param>
        /// <returns>True if the parsing was completed successfully, false otherwise.</returns>
        private bool TryParseCompilerMessage(string messageText, out CompilerMessage message)
        {
            message = new CompilerMessage();

            Match matchCompile = compileErrorRegex.Match(messageText);
            if (matchCompile.Success)
            {
                message.file = matchCompile.Groups["file"].Value;
                message.line = Int32.Parse(matchCompile.Groups["line"].Value);
                message.column = Int32.Parse(matchCompile.Groups["column"].Value);
                message.type = matchCompile.Groups["type"].Value == "error"
                    ? CompilerMessageType.Error
                    : CompilerMessageType.Warning;
                message.message = matchCompile.Groups["message"].Value;

                return true;
            }

            Match matchCompiler = compilerErrorRegex.Match(messageText);
            if (matchCompiler.Success)
            {
                message.file = "";
                message.line = 0;
                message.column = 0;
                message.type = CompilerMessageType.Error;
                message.message = matchCompiler.Groups["message"].Value;

                return true;
            }

            return false;
        }

        /// <summary>
        /// Checks is the compilation process done.
        /// </summary>
        public bool IsDone
        {
            get { return process.HasExited && readErrorsThread.ThreadState == System.Threading.ThreadState.Stopped; }
        }

        /// <summary>
        /// Checks has the compilAtion had any errors. Only valid after <see cref="IsDone"/> returns true.
        /// </summary>
        public bool HasErrors
        {
            get { return IsDone && process.ExitCode != 0; }
        }

        /// <summary>
        /// Returns all warning messages generated by the compiler.
        /// </summary>
        public CompilerMessage[] WarningMessages
        {
            get
            {
                lock (warnings)
                {
                    return warnings.ToArray();
                }
            }
        }

        /// <summary>
        /// Returns all error messages generated by the compiler.
        /// </summary>
        public CompilerMessage[] ErrorMessages
        {
            get
            {
                lock (errors)
                {
                    return errors.ToArray();
                }
            }
        }

        /// <summary>
        /// Disposes of the compiler process. Should be called when done when this object instance.
        /// </summary>
        public void Dispose()
        {
            if (process == null)
                return;

            if (!process.HasExited)
            {
                process.Kill();
                process.WaitForExit();
            }

            process.Dispose();
        }
    }

    /// <summary>
    /// Type of messages reported by the script compiler.
    /// </summary>
    public enum CompilerMessageType
    {
        Warning, Error
    }

    /// <summary>
    /// Data about a message reported by the compiler.
    /// </summary>
    public struct CompilerMessage
    {
        /// <summary>Type of the message.</summary>
        public CompilerMessageType type;
        /// <summary>Body of the message.</summary>
        public string message;
        /// <summary>Path ot the file the message is referencing.</summary>
        public string file;
        /// <summary>Line the message is referencing.</summary>
        public int line;
        /// <summary>Column the message is referencing.</summary>
        public int column;
    }

    /** @} */
}
